package com.creatified.jacqueline;

import java.util.regex.*;

import org.apache.log4j.Logger;

/**
 * A JacqulineSelector is a special kind of CSS selector which can also be translated into a XPath expression.
 */
public final class JacquelineSelector {

	private String selectorText;

	private String url;

	private int line;

	private String xPathExpression;

	private int timesUsed = 0;

	protected static final Logger log = Logger.getLogger(JacquelineSelector.class);

	/**
	 * Creates a new JacquelineSelector instance
	 * 
	 * @param selectorText the selector text
	 * @param url the url
	 * @param line the line
	 */
	protected JacquelineSelector(String selectorText, String url, int line) throws JacquelineException {
		this(selectorText, url, line, JacquelineSelector.generateXPathExpression(selectorText));
	}

	/**
	 * Creates a new JacquelineSelector instance
	 * 
	 * @param selectorText the selector text
	 * @param url the url
	 * @param line the line
	 * @param xPathExpression the x path expression
	 */
	protected JacquelineSelector(String selectorText, String url, int line, String xPathExpression) {
		log.debug("creating new Selector '" + selectorText + "'");
		this.xPathExpression = xPathExpression;
		this.selectorText = selectorText;
		this.url = url;
		this.line = line;
	}

	/**
	 * Gets the selector text.
	 * 
	 * @return the selector text
	 */
	public String getSelectorText() {
		return selectorText;
	}

	/**
	 * Gets the times used.
	 * 
	 * @return the times used
	 */
	public int getTimesUsed() {
		return timesUsed;
	}

	/**
	 * Sets the times used.
	 * 
	 * @param timesUsed the times used
	 * 
	 * @return true, if successful
	 */
	protected boolean setTimesUsed(int timesUsed) {
		if(timesUsed >= 0) {
			this.timesUsed = timesUsed;
			return true;
		}
		log.error("Tried to set 'timesUsed' to a negative value.");
		return false;
	}

	/**
	 * Gets the url.
	 * 
	 * @return the url
	 */
	public String getUrl() {
		return url;
	}

	/**
	 * Gets the line.
	 * 
	 * @return the line
	 */
	public Integer getLine() {
		return line;
	}

	/**
	 * Gets the x path expression.
	 * 
	 * @return the x path expression
	 */
	public String getXPathExpression() {
		return xPathExpression;
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	public String toString() {
		return this.selectorText + " (" + this.getXPathExpression() + ")";
	}

	/**
	 * Utility method to translate CSS2 selectors or CSS2 rules into XPath expressions
	 * 
	 * @param cssSelector the css selector to translate
	 * 
	 * @return the generated XPath expression
	 */
	protected static String generateXPathExpression(String cssSelector) throws JacquelineException {

		log.debug("generating XPathExpression for '" + cssSelector + "'");

		//Start with an empty StringBuilder for the XPath query
		StringBuilder resultingXpath = new StringBuilder();


		resultingXpath.append(".");

		String[] selectors = cssSelector.split("\\s");

		//Iterate over all parts of this selector
		for(int j = 0; j < selectors.length; j++) {

			if(selectors[j].matches("(\\*#).*")) {
				log.warn("Selector part '" + selectors[j] + "' was in bad syntax - maybe parsing error by Batik.");
				selectors[j] = selectors[j].substring(1);
			}

			Pattern selectorPattern = Pattern.compile(regSelector);
			Matcher selector = selectorPattern.matcher(selectors[j]);


			if(!selector.matches()) {
				throw new JacquelineException("Ignored malformed selector '" + cssSelector + "'");
			}

			splitTag = ((selector.group(1) == null || (selector.group(3) != null && selector.group(3).equals("*")))? "*" : selector.group(1));
			splitId = (selector.group(2) != null && !selector.group(2).equals("*")) ? selector.group(2) : "";
			splitAllClasses = selector.group(4);
			splitAllAttr = selector.group(6);
			splitAllPseudos = selector.group(11);
			splitTagRelation = selector.group(23);

			if(splitTagRelation != null) {
				if(splitTagRelation.equals(">")) {
					resultingXpath.append("/child::");
				} else if(splitTagRelation.equals("+")) {
					resultingXpath.append("/following-sibling::*[1]/self::");
				} else if(splitTagRelation.equals("~")) {
					resultingXpath.append("/following-sibling::");
				}
			} else {
				resultingXpath.append((j > 0 && selectors[j-1].matches(regRelation) ? splitTag : ("/descendant::" + splitTag)));
			}

			if(splitId != null && !splitId.isEmpty()) {
				resultingXpath.append("[@id = \"" + splitId.replaceAll("^#", "").replaceAll("\\\\", "") + "\"]");
			}

			if(splitAllClasses != null && !splitAllClasses.isEmpty()) {
				resultingXpath.append(splitAllClasses.replaceAll(regClasses, "[contains(concat(\" \", @class, \" \"), \" $1 \")]"));
			}

			if(splitAllAttr != null && !splitAllAttr.isEmpty()) {
				Matcher m = Pattern.compile(regAttribs).matcher(splitAllAttr);
				if(m.matches()) {
					resultingXpath.append(splitAllAttr.replaceAll(regAttribs, selectorAttributeToXPathExtended(splitAllAttr, m.group(1), m.group(2), m.group(3))));
				}
			}

			if(splitAllPseudos != null && !splitAllPseudos.isEmpty()) {
				Pattern pseudoPattern = Pattern.compile(regPseudos);
				Matcher allPseudos = pseudoPattern.matcher(splitAllPseudos);

				if(allPseudos.matches()) {
					for (int k = 0; k < allPseudos.groupCount(); k++) {
						if(allPseudos.group(k) != null) {
							Matcher pseudo = pseudoPattern.matcher(allPseudos.group(k));
							if(pseudo.matches()) {

								String pseudoClass = (pseudo.group(1) != null ? pseudo.group(1).toLowerCase() : null);
								String pseudoValue = (pseudo.group(3) != null ? pseudo.group(3) : null);

								String xpathA = pseudoClassToXPath(splitTag, pseudoClass, pseudoValue);

								if(!xpathA.isEmpty()) {
									resultingXpath.append("[" + xpathA + "]");
								}
							}
						}
					}
				}
			}

		}
		if(resultingXpath != null) {
			return resultingXpath.toString();
		} else {
			return null;
		}
	}

	/**
	 * Attr to x path.
	 * 
	 * @param match the match
	 * @param p1 the p1
	 * @param p2 the p2
	 * @param p3 the p3
	 * 
	 * @return the string
	 */
	private static String selectorAttributeToXPath (String match, String p1, String p2, String p3) {
		p3 = (p3 != null && !p3.isEmpty()) ? p3.replaceAll(regQuoted, "$1") : p3;
		if(p2 != null && p2.equals("^")) {
			return "starts-with(@" + p1 + ", \"" + p3 + "\")";
		} else if(p2 != null && p2.equals("$")) {
			return "substring(@" + p1 + ", (string-length(@" + p1 + ") - " + (p3.length() - 1) + "), " + p3.length() + ") = \"" + p3 + "\"";
		} else if(p2 != null && p2.equals("*")) {
			return "contains(concat(\" \", @" + p1 + ", \" \"), \"" + p3 + "\")";
		} else if(p2 != null && p2.equals("|")) {
			return "(@" + p1 + "=\"" + p3 + "\" or starts-with(@" + p1 + ", \"" + p3 + "-\"))";
		} else if(p2 != null && p2.equals("~")) {
			return "contains(concat(\" \", @" + p1 + ", \" \"), \" " + p3 + " \")";
		} else {
			return "@" + p1 + ((p3 != null && !p3.isEmpty()) ? "=\"" + p3 + "\"" : "");
		}
	}

	/**
	 * Attr to x path b.
	 * 
	 * @param match the match
	 * @param p1 the p1
	 * @param p2 the p2
	 * @param p3 the p3
	 * 
	 * @return the string
	 */
	private static String selectorAttributeToXPathExtended (String match, String p1, String p2, String p3) {
		return "[" + selectorAttributeToXPath(match, p1, p2, p3) + "]";
	}

	/**
	 * Pseudo to x path.
	 * 
	 * @param tag the tag
	 * @param pseudoClass the pseudo class
	 * @param pseudoValue the pseudo value
	 * 
	 * @return the string
	 */
	private static String pseudoClassToXPath (String tag, String pseudoClass, String pseudoValue) {
		tag = pseudoClass.endsWith("-child") ? "*" : tag;
		StringBuilder xpath = new StringBuilder();
		String[] pseudo = pseudoClass.split("-");

		if(pseudo[0].equals("nth")) {
			if(pseudoValue.matches("!^n$")) {
				// ignore this! --> CSS3
			}
		} else if(pseudo[0].equals("not")) {
			//ignore this! --> CSS3
		} else if(pseudo[0].equals("first")) {
			return "not(preceding-sibling::" + tag + ")";
		} else if(pseudo[0].equals("last")) {
			//ignore this! --> CSS3
		} else if(pseudo[0].equals("only")) {
			//ignore this! --> CSS3
		} else if(pseudo[0].equals("empty")) {
			//ignore this! --> CSS3
		} else if(pseudo[0].equals("contains")) {
			//ignore this! --> CSS3
		} else if(pseudo[0].equals("enabled")) {
			//ignore this! --> CSS3
		} else if(pseudo[0].equals("disabled")) {
			//ignore this! --> CSS3
		} else if(pseudo[0].equals("target")) {
			//ignore this! --> CSS3
		} else {
			return "@" + pseudoClass + "=\"" + pseudoValue + "\"";
		}

		return xpath.toString();
	}

	//  private static String regRules = "\\s*(,)\\s*";
	private static String regSelector = "^(\\w+)?(#[\\w\\u00C0-\\uFFFF\\-\\_\\\\\\.]+|(\\*))?((\\.[\\w\\u00C0-\\uFFFF\\-_]+)*)?((\\[\\w+\\s*(\\^|\\$|\\*|\\||~)?(=\\s*([\\w\\u00C0-\\uFFFF\\s\\-\\_\\.]+|\\\"[^\\\"]*\\\"|'[^']*'))?\\]+)*)?(((:\\w+[\\w\\-]*)(\\((odd|even|\\-?\\d*n?((\\+|\\-)\\d+)?|[\\w\\u00C0-\\uFFFF\\-_\\.]+|\\\"[^\\\"]*\\\"|'[^']*'|((\\w*\\.[\\w\\u00C0-\\uFFFF\\-_]+)*)?|(\\[#?\\w+(\\^|\\$|\\*|\\||~)?=?[\\w\\u00C0-\\uFFFF\\s\\-\\_\\.\\'\\\\\"]+\\]+)|(:\\w+[\\w\\-]*))\\))?)*)?(>|\\+|~)?";

	private static String regQuoted = "^[\"'](.*)[\"']$";
	//	private static String regId = "^#([\\w\\u00C0-\\uFFFF\\-\\_]+)$";
	//	private static String regTag = "^(\\w+)";
	private static String regRelation = "^(>|\\+|~)$";
	//	private static String regPseudo = "^:(\\w[\\w\\-]*)(\\((.+)\\))?$";
	private static String regPseudos = ":(\\w[\\w\\-]*)(\\(([^\\)]+)\\))?";
	private static String regAttribs = "\\[(\\w+)\\s*(\\^|\\$|\\*|\\||~)?=?\\s*([\\w\\u00C0-\\uFFFF\\s\\-_\\.]+|\"[^\"]*\"|'[^']*')?\\]";
	private static String regClasses = "\\.([\\w\\u00C0-\\uFFFF\\-_]+)";
	//	private static String regNth = "^((odd|even)|([1-9]\\d*)|((([1-9]\\d*)?)n([\\+\\-]\\d+)?)|(\\-(([1-9]\\d*)?)n\\+(\\d+)))$";
	//	private static String quoted = "^[\"'](.*)[\"']$";
	private static String splitTag, splitId, splitAllClasses, splitAllAttr, splitAllPseudos, splitTagRelation = new String("");

}
